iT邦幫忙

2024 iThome 鐵人賽

DAY 12
2
Modern Web

為你自己寫 Vue Component系列 第 12

[為你自己寫 Vue Component] AtomicBadge

  • 分享至 

  • xImage
  •  

[為你自己寫 Vue Component] AtomicBadge

Badge 元件是一種簡單但功能強大的資料展示元件,通常以小圓形的形式出現,依附於其他 UI 元件上。Badge 元件主要用於通知提醒,顯示未讀訊息、通知數量,或是狀態指示如上線中、離線、忙碌等。

AtomicBadge Demo

元件分析

元件架構

AtomicBadge 架構圖

  1. Container:Badge 的容器,用來包裹其他元件。
  2. Badge:Badge。

功能設計

在開始實作前,我們先研究各個 UI Library 的 Badge 元件是如何設計的。

Element Plus

Element Plus Badge

<template>
  <ElBadge :value="12" class="item">
    <ElButton>Button</ElButton>
  </ElBadge>
  <ElBadge :value="99" :max="9" class="item">
    <ElButton>Button</ElButton>
  </ElBadge>
  <ElBadge :value="1" class="item" is-dot>
    <ElButton>Button</ElButton>
  </ElBadge>
  <ElBadge :value="2" class="item" type="warning">
    <ElButton>Button</ElButton>
  </ElBadge>
  <ElBadge :value="1" class="item" color="green">
    <ElButton>Button</ElButton>
  </ElBadge>
</template>

Element Plus 選用了 colortype 來設定 Badge 的樣式,value 用來設定 Badge 的內容,如果希望只顯示的是圓點可以設定 is-dot,最後 max 可以設定顯示數字的最大值,如果設定為 9,當 value 超過這個數值則只會顯示 9+。

colortype 在這裡設定的都是 Badge 的顏色,不過看起來 color 的權重更高一點,並且可以傳入任何 CSS 顏色值。

Vuetify

Vuetify Badge

<template>
  <VBtn class="text-none" stacked>
    <VBadge color="success" dot>
      <VIcon>mdi-home-outline</VIcon>
    </VBadge>
  </VBtn>

  <VBtn class="text-none" stacked>
    <VBadge color="error" content="200" max="9">
      <VIcon>mdi-store-outline</VIcon>
    </VBadge>
  </VBtn>

  <VBtn class="text-none" stacked>
    <VBadge color="error" content="2">
      <VIcon>mdi-bell-outline</VIcon>
    </VBadge>
  </VBtn>
</template>

Vuetify 的 Badge 元件也是以 color 來設定 Badge 的顏色,content 用來設定 Badge 的內容,而圓點樣式可以使用 dot 來開啟,一樣支援 max 設定。

Nuxt UI

<template>
  <UChip size="2xl">
    <UButton icon="i-heroicons-inbox" color="gray" />
  </UChip>
</template>

在 Nuxt UI 中,這個元件的名稱叫做 <UChip>,除了名稱之外其他部分與 Element Plus 跟 Vuetify 都差不多。

另外,Nuxt UI 可以透過 position 來設定 Badge 的位置,Vuetify 也可以透過 location 設定,而 Element Plus 從目前的文件上看起來尚未支援定位的功能。

綜合以上並結合自身經驗,我們統整出 <AtomicBadge> 的功能:

  • 可以透過 content 設定 Badge 要顯示的內容。
  • 可以透過 max 設定最大顯示數字。
  • 可以透過 placement 設定 Badge 的位置,支援 top-lefttop-rightbottom-leftbottom-right 四個位置。
  • 可以透過 size 設定 Badge 的大小,這裡除了 mediumlarge 外,也將 dot 整合在這個設定裡面。
  • 可以透過 color 設定 Badge 的顏色。
  • 可以透過 showZero 決定當 content 為 0 時是否顯示 Badge。

使用結構如下:

<template>
  <AtomicBadge
    color="primary"
    content="12"
    max="9"
    placement="top-right"
    size="medium"
  >
    <AtomicAvatar size="60" />
  </AtomicBadge>
</template>

元件實作

首先,我們將需求中提到的功能整理成 props 的介面,我們會需要下列屬性:

名稱 型別 預設值 說明
content string undefined Badge 的內容
max number 99 最大顯示數字
placement top-left, top-right, bottom-left, bottom-right top-right Badge 的位置
size medium, large, dot medium Badge 的大小
color primary, success, warning, danger, info primary Badge 的顏色
showZero boolean false 是否顯示 0 的 Badge
type Numberish = number | `${number}`;

interface AtomicBadgeProps {
  content?: string | number | null;
  max?: Numberish;
  placement?:
    | 'top-left'
    | 'top-right'
    | 'bottom-left'
    | 'bottom-right';
  size?: 'dot' | 'medium' | 'large';
  color?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
  showZero?: boolean;
}

const props = withDefaults(defineProps<AtomicBadgeProps>(), {
  content: undefined,
  max: 99,
  placement: 'top-right',
  size: 'medium',
  color: 'danger',
  showZero: false,
});

首先我們來規劃元件的模板

<template>
  <span class="atomic-badge">
    <slot name="default" />
    <span class="atomic-badge__content">
      {{ content }}
    </span>
  </span>
</template>

Content 的部分我們要加上顯示判斷

  • 如果 contentnullundefined 則不顯示。
  • 如果 content 為數字,要檢查有沒有超出 max 的值,如果超出則顯示 {max}+
  • 如果 sizedot 則不顯示 content
<span class="atomic-badge__content">
  <template v-if="!isNullOrUndefined(content) && size !== 'dot'">
    {{ Number(content) > Number(max) ? `${max}+` : content }}
  </template>
</span>

接著我們來處理 Badge 的樣式。關於樣式有 Badge 的位置、大小、顏色要處理。

const invisible = computed(() => {
  const { content, showZero } = props;
  return !showZero && Number(content) === 0;
});

const CONTENT_CLASS = 'atomic-badge__content';
const contentClass = computed(() =>
  [
    `${CONTENT_CLASS}--${props.placement}`,
    `${CONTENT_CLASS}--${props.size}`,
    `${CONTENT_CLASS}--${props.color}`,
    invisible.value ? `${CONTENT_CLASS}--invisible` : '',
  ].join(' ')
);
<template>
  <span class="atomic-badge">
    <slot name="default" />
    <span
      class="atomic-badge__content"
      :class="contentClass"
    >
      <!-- 略 -->
    </span>
  </span>
</template>

大小跟顏色的部份我們應該都很熟悉了!這裡主要要處理的是 Badge 的位置。

基本樣式

.atomic-badge {
  position: relative;

  &__content {
    position: absolute;
  }
}
.atomic-badge {
  &__content {
    &--top-right {
      top: 0;
      right: 0;
      transform: translateX(50%) translateY(-50%);
    }
  }
}

但還有一個要在這裡一併處理的是 showZero 的情況,在這裡為了讓 content 從 0 到 1 的過程中有縮放效果,我們可選用 transform: scale(0)transform: scale(1) 的方式來處理。

所以原本的樣式就要稍微擴充一下:

.atomic-badge {
  position: relative;

  &__content {
    position: absolute;

    &--top-right {
      top: 0;
      right: 0;
      transform: translateX(50%) translateY(-50%) scale(1);
    }

    &--top-right#{&}--invisible {
      transform: translateX(50%) translateY(-50%) scale(0);
    }
  }
}

不多,我們只要重複四次就好了?!

如果跟我一樣覺得這樣很冗長很煩的話,可以參考看看這個受 Tailwind CSS 啟發的作法,我們先新增一個 %transform 的樣式

%transform {
  transform: translateX(var(--badge-translate-x)) translateY(var(--badge-translate-y))
    scale(var(--badge-scale));
}

再新增 %top%right 的樣式

%top {
  --badge-translate-y: -50%;
  top: 0;
}

%right {
  --badge-translate-x: 50%;
  right: 0;
}

整合起來會像是這樣:

.atomic-badge {
  position: relative;

  &__content {
    --badge-scale: 1;

    position: absolute;
    @extend %transform;

    &--invisible {
      --badge-scale: 0;
    }

    &--top-right {
      @extend %top;
      @extend %right;
    }

    &--top-left {
      @extend %top;
      @extend %left;
    }
  }
}

這樣看起來就簡潔許多了呢!

進階功能

在現在的版本中,我們的 <AtomicBadge> 遇到圓形的 UI 會離邊界有一點距離

Atomic Badge Advance Demo

如果能支援貼到邊上的話就太好了!要做到這個功能我們得拿出紙筆算一下數學,我們要算出 <AtomicBadge> 遇到圓形的時候,topright 要偏移的百分比。

Atomic Badge Advance Geometry

第一步,假設圓的直徑為 2,我們得先算出紅色三角形的斜邊長。

在這裡的紅色三角形是一個「等腰直角三角形」,所以斜邊長與對邊(臨邊)長為 1 : 1 : √2,所以斜邊長為 2√2

第二步,斜邊長減掉圓的直徑 2 並除以 2,就會算出虛線方形的對角線長度

$$\frac{2\sqrt{2} - 2}{2} = \frac{2(\sqrt{2} - 1)}{2} = \sqrt{2} - 1$$

第三步,根據第一步提到的 1 : 1 : √2 這個比例我們知道,當我們將上一步的結果除以 √2 就會得到 topright 的值。

$$\frac{\sqrt{2} - 1}{\sqrt{2}} = 1 - \frac{1}{\sqrt{2}}$$

再來,√2 約等於 1.414,所以我們可以算出

$$1 - \frac{1}{1.414} = 1 - 0.707 = 0.293$$

一開始我們的圓直徑是 2,所以我們要將這個值除以 2 乘以 100 就會得到 topright 要偏移的百分比。

$$\frac{0.293}{2} \times 100 = 14.65$$

所以我們可以算出來,如果要讓 <AtomicBadge> 貼到圓形邊上,topright 要偏移的百分比為 14.65%

我們新增一個 propsoverlap,讓使用者決定要使用圓形還是方形定位的 Badge。

interface AtomicBadgeProps {
  overlap?: 'circular' | 'rectangular';
}

const props = withDefaults(defineProps<AtomicBadgeProps>(), {
  overlap: 'circular',
});

const CONTENT_CLASS = 'atomic-badge__content';
const contentClass = computed(() =>
  [
    `${CONTENT_CLASS}--${props.overlap}`,
    // 略
  ].join(' ')
);
.atomic-badge {
  position: relative;

  &__content {
    // 略

    &--rectangular {
      --badge-offset: 0;
    }

    &--circular {
      --badge-offset: 14.65%;
    }
  }
}

%top {
  --badge-translate-y: -50%;
  top: var(--badge-offset);
}

%right {
  --badge-translate-x: 50%;
  right: var(--badge-offset);
}

這樣我們就可以讓使用者在遇到圓形的時候,可以選擇要貼到邊上還是留一點距離。

總結

這次我們實作了一個 <AtomicBadge> 元件,這個元件可以讓我們在 UI 上加上一個小小的提示,讓使用者知道這個元件有一些特殊的狀態。

在實作元件定位的過程中我們借鑒了 Tailwind CSS 處理 Util Class 的作法,免去了我們不斷定義重複樣式的麻煩,這樣的作法讓我們的樣式更加簡潔。

最後我們也實作了一個進階需求,讓使用者可以選擇要讓 <AtomicBadge> 貼到圓形邊上還是留一點距離,過程中用了基礎的幾何計算,強烈建議遇到計算問題實際拿紙筆算一次或是自己畫圖畫一次,這樣更能深刻吸收並且應用在其他地方。

參考資料


上一篇
[為你自己寫 Vue Component] AtomicAvatar
下一篇
[為你自己寫 Vue Component] AtomicChip
系列文
為你自己寫 Vue Component19
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言